winbrew_app\operations\install/
sevenz.rs1use anyhow::{Context, Result};
2use std::env;
3use std::ffi::OsString;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8use crate::core::env::WINBREW_PATHS_ROOT;
9use crate::core::fs::{cleanup_path, replace_directory};
10use crate::core::hash::{hash_file, verify_hash};
11use crate::core::network::{build_client, download_url_to_temp_file, is_7z_path};
12use crate::core::paths::system_sevenz_binary_path;
13pub(crate) use crate::core::paths::{
14 sevenz_bin_path_from_runtime_root, sevenz_dll_path_from_runtime_root,
15 sevenz_runtime_dir_from_runtime_root,
16};
17use crate::models::shared::hash::HashAlgorithm;
18
19use super::InstallError;
20
21const SEVENZ_BOOTSTRAP_USER_AGENT: &str = "WinBrew 7-Zip bootstrap";
22const SEVENZ_BOOTSTRAP_VERSION: &str = "26.00";
23const SEVENZ_VERSION_FILENAME: &str = "VERSION";
24const SEVENZR_FILENAME: &str = "7zr.exe";
25const SEVENZR_DOWNLOAD_SHA256: &str =
26 "sha256:4bec0bc59836a890a11568b58bd12a3e7b23a683557340562da211b6088058ba";
27const SEVENZ_X86_DOWNLOAD_SHA256: &str =
28 "sha256:d605eb609aa67796dca7cfe26d7e28792090bb8048302d6e05ede16e8e33145c";
29
30fn sevenzr_download_url() -> String {
31 format!(
32 "https://github.com/ip7z/7zip/releases/download/{SEVENZ_BOOTSTRAP_VERSION}/{SEVENZR_FILENAME}"
33 )
34}
35
36fn sevenz_x86_download_url() -> String {
37 let x86_filename = sevenz_x86_filename();
38 format!(
39 "https://github.com/ip7z/7zip/releases/download/{SEVENZ_BOOTSTRAP_VERSION}/{x86_filename}"
40 )
41}
42
43fn sevenz_x86_filename() -> String {
44 format!("7z{}.exe", SEVENZ_BOOTSTRAP_VERSION.replace('.', ""))
45}
46
47pub(crate) fn sevenz_version_manifest_path(runtime_root: &Path) -> PathBuf {
48 sevenz_runtime_dir_from_runtime_root(runtime_root).join(SEVENZ_VERSION_FILENAME)
49}
50
51pub(crate) fn runtime_root_env_guard(root: &Path) -> RuntimeRootEnvGuard {
52 RuntimeRootEnvGuard::set(WINBREW_PATHS_ROOT, root)
53}
54
55pub(crate) fn ensure_runtime(
56 runtime_root: &Path,
57 installer_url: &str,
58 mut confirm_runtime_bootstrap: impl FnMut(&str, &Path) -> Result<bool>,
59) -> Result<(), InstallError> {
60 if !runtime_bootstrap_required(runtime_root, installer_url) {
61 return Ok(());
62 }
63
64 let runtime_dir = sevenz_runtime_dir_from_runtime_root(runtime_root);
65 if !confirm_runtime_bootstrap("7-Zip runtime", &runtime_dir)? {
66 return Err(InstallError::RuntimeBootstrapDeclined {
67 runtime: "7-Zip runtime".to_string(),
68 });
69 }
70
71 bootstrap_local_runtime(runtime_root).map_err(InstallError::from)
72}
73
74pub(crate) fn runtime_bootstrap_required(runtime_root: &Path, installer_url: &str) -> bool {
75 is_7z_path(installer_url)
76 && system_sevenz_binary_path().is_none()
77 && !local_runtime_available(runtime_root)
78}
79
80fn local_runtime_available(runtime_root: &Path) -> bool {
81 sevenz_bin_path_from_runtime_root(runtime_root).exists()
82 && sevenz_dll_path_from_runtime_root(runtime_root).exists()
83 && local_runtime_version_matches(runtime_root)
84}
85
86fn local_runtime_version_matches(runtime_root: &Path) -> bool {
87 let version_path = sevenz_version_manifest_path(runtime_root);
88 std::fs::read_to_string(&version_path)
89 .map(|content| content.trim() == SEVENZ_BOOTSTRAP_VERSION)
90 .unwrap_or(false)
91}
92
93fn bootstrap_local_runtime(runtime_root: &Path) -> Result<()> {
94 let target_dir = sevenz_runtime_dir_from_runtime_root(runtime_root);
95 let staging_dir = create_bootstrap_root();
96 let sevenzr_path = staging_dir.join(SEVENZR_FILENAME);
97 let sevenz_x86_filename = sevenz_x86_filename();
98 let installer_path = staging_dir.join(&sevenz_x86_filename);
99 let artifacts = BootstrapArtifacts::new(
100 staging_dir.clone(),
101 sevenzr_path.clone(),
102 installer_path.clone(),
103 );
104
105 fs::create_dir_all(&staging_dir).with_context(|| {
106 format!("failed to create 7z bootstrap staging directory {staging_dir:?}")
107 })?;
108
109 let client = build_client(SEVENZ_BOOTSTRAP_USER_AGENT)
110 .context("failed to build 7z bootstrap HTTP client")?;
111
112 let sevenzr_url = sevenzr_download_url();
113 let sevenz_x86_url = sevenz_x86_download_url();
114
115 download_verified_asset(
116 &client,
117 &sevenzr_url,
118 &sevenzr_path,
119 SEVENZR_FILENAME,
120 SEVENZR_DOWNLOAD_SHA256,
121 )?;
122 download_verified_asset(
123 &client,
124 &sevenz_x86_url,
125 &installer_path,
126 &sevenz_x86_filename,
127 SEVENZ_X86_DOWNLOAD_SHA256,
128 )?;
129
130 run_bootstrap_extractor(&sevenzr_path, &installer_path, &staging_dir)?;
131
132 let version_path = staging_dir.join(SEVENZ_VERSION_FILENAME);
133 fs::write(&version_path, SEVENZ_BOOTSTRAP_VERSION).with_context(|| {
134 format!(
135 "failed to write 7z bootstrap version file at {}",
136 version_path.display()
137 )
138 })?;
139
140 if let Some(parent) = target_dir.parent() {
141 fs::create_dir_all(parent).with_context(|| {
142 format!(
143 "failed to create parent directory for {}",
144 target_dir.display()
145 )
146 })?;
147 }
148
149 replace_directory(&staging_dir, &target_dir)
150 .with_context(|| format!("failed to publish 7z runtime into {}", target_dir.display()))?;
151
152 artifacts.commit();
153 Ok(())
154}
155
156fn download_verified_asset(
157 client: &crate::core::network::Client,
158 url: &str,
159 temp_path: &Path,
160 label: &str,
161 expected_hash: &str,
162) -> Result<()> {
163 download_url_to_temp_file(
164 client,
165 url,
166 temp_path,
167 label,
168 |_| {},
169 |_| {},
170 |_| Ok::<(), crate::core::network::BoxError>(()),
171 )
172 .with_context(|| format!("failed to download {label}"))?;
173
174 let actual_hash = hash_file(temp_path, HashAlgorithm::Sha256)
175 .with_context(|| format!("failed to hash downloaded {label}"))?;
176
177 verify_hash(expected_hash, actual_hash)
178 .with_context(|| format!("downloaded {label} hash mismatch"))?;
179
180 Ok(())
181}
182
183fn run_bootstrap_extractor(
184 sevenzr_path: &Path,
185 archive_path: &Path,
186 destination_dir: &Path,
187) -> Result<()> {
188 let status = Command::new(sevenzr_path)
189 .arg("x")
190 .arg("-y")
191 .arg("-bd")
192 .arg(format!("-o{}", destination_dir.display()))
193 .arg(archive_path)
194 .arg("7z.exe")
195 .arg("7z.dll")
196 .status()
197 .with_context(|| {
198 format!(
199 "failed to launch 7z bootstrap extractor at {}",
200 sevenzr_path.display()
201 )
202 })?;
203
204 if status.success() {
205 Ok(())
206 } else {
207 anyhow::bail!("7zr exited with status {status}");
208 }
209}
210
211fn create_bootstrap_root() -> PathBuf {
212 let mut bootstrap_root = env::temp_dir();
213 bootstrap_root.push(format!(
214 "winbrew-7zip-bootstrap-{}-{}",
215 std::process::id(),
216 std::time::SystemTime::now()
217 .duration_since(std::time::UNIX_EPOCH)
218 .unwrap_or_default()
219 .as_nanos()
220 ));
221
222 bootstrap_root
223}
224
225struct BootstrapArtifacts {
226 staging_dir: PathBuf,
227 sevenzr_path: PathBuf,
228 installer_path: PathBuf,
229 committed: bool,
230}
231
232impl BootstrapArtifacts {
233 fn new(staging_dir: PathBuf, sevenzr_path: PathBuf, installer_path: PathBuf) -> Self {
234 Self {
235 staging_dir,
236 sevenzr_path,
237 installer_path,
238 committed: false,
239 }
240 }
241
242 fn commit(mut self) {
243 let _ = cleanup_path(&self.staging_dir);
244 let _ = fs::remove_file(&self.sevenzr_path);
245 let _ = fs::remove_file(&self.installer_path);
246 self.committed = true;
247 }
248}
249
250impl Drop for BootstrapArtifacts {
251 fn drop(&mut self) {
252 if !self.committed {
253 let _ = cleanup_path(&self.staging_dir);
254 let _ = fs::remove_file(&self.sevenzr_path);
255 let _ = fs::remove_file(&self.installer_path);
256 }
257 }
258}
259
260pub(crate) struct RuntimeRootEnvGuard {
261 key: &'static str,
262 previous: Option<OsString>,
263}
264
265impl RuntimeRootEnvGuard {
266 fn set(key: &'static str, value: &Path) -> Self {
267 let previous = env::var_os(key);
268 unsafe {
269 env::set_var(key, value);
270 }
271
272 Self { key, previous }
273 }
274}
275
276impl Drop for RuntimeRootEnvGuard {
277 fn drop(&mut self) {
278 if let Some(previous) = self.previous.take() {
279 unsafe {
280 env::set_var(self.key, previous);
281 }
282 } else {
283 unsafe {
284 env::remove_var(self.key);
285 }
286 }
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use std::fs;
294 use tempfile::tempdir;
295
296 #[test]
297 fn bootstrap_urls_derive_from_version() {
298 let expected_version = SEVENZ_BOOTSTRAP_VERSION;
299 let expected_x86_filename = sevenz_x86_filename();
300 assert_eq!(
301 sevenzr_download_url(),
302 format!(
303 "https://github.com/ip7z/7zip/releases/download/{expected_version}/{SEVENZR_FILENAME}"
304 )
305 );
306 assert_eq!(
307 sevenz_x86_download_url(),
308 format!(
309 "https://github.com/ip7z/7zip/releases/download/{expected_version}/{expected_x86_filename}"
310 )
311 );
312 }
313
314 #[test]
315 fn local_runtime_available_requires_matching_version_manifest() -> Result<()> {
316 let temp_dir = tempdir().expect("temp dir");
317 let runtime_root = temp_dir.path();
318 let runtime_dir = sevenz_runtime_dir_from_runtime_root(runtime_root);
319 fs::create_dir_all(&runtime_dir)?;
320
321 fs::write(runtime_dir.join("7z.exe"), b"")?;
322 fs::write(runtime_dir.join("7z.dll"), b"")?;
323 fs::write(sevenz_version_manifest_path(runtime_root), b"25.50")?;
324
325 assert!(!local_runtime_available(runtime_root));
326
327 fs::write(
328 sevenz_version_manifest_path(runtime_root),
329 SEVENZ_BOOTSTRAP_VERSION,
330 )?;
331 assert!(local_runtime_available(runtime_root));
332
333 Ok(())
334 }
335
336 #[test]
337 fn runtime_bootstrap_required_rejects_mismatched_local_version() -> Result<()> {
338 let temp_dir = tempdir().expect("temp dir");
339 let runtime_root = temp_dir.path();
340 let runtime_dir = sevenz_runtime_dir_from_runtime_root(runtime_root);
341
342 fs::create_dir_all(&runtime_dir)?;
343 fs::write(runtime_dir.join("7z.exe"), b"")?;
344 fs::write(runtime_dir.join("7z.dll"), b"")?;
345 fs::write(sevenz_version_manifest_path(runtime_root), b"25.50")?;
346
347 assert!(runtime_bootstrap_required(
348 runtime_root,
349 "https://example.invalid/archive.7z"
350 ));
351
352 fs::write(
353 sevenz_version_manifest_path(runtime_root),
354 SEVENZ_BOOTSTRAP_VERSION,
355 )?;
356 assert!(!runtime_bootstrap_required(
357 runtime_root,
358 "https://example.invalid/archive.7z"
359 ));
360
361 Ok(())
362 }
363}